"""libretro runner"""
# Standard Library
import os
from operator import itemgetter
from zipfile import ZipFile

# Third Party Libraries
import requests

# Lutris Modules
from lutris import settings
from lutris.runners.runner import Runner
from lutris.util import system
from lutris.util.libretro import RetroConfig
from lutris.util.log import logger


def get_default_config_path(path=""):
    return os.path.join(settings.RUNNER_DIR, "retroarch", path)


# List of supported libretro cores
# First element is the human readable name for the core with the platform's short name
# Second element is the core identifier
# Third element is the platform's long name
LIBRETRO_CORES = []
runner_path = get_default_config_path()
if os.path.exists(runner_path):
    # Get core identifiers from info dir
    info_path = get_default_config_path("info")
    if not os.path.exists(info_path):
        req = requests.get("http://buildbot.libretro.com/assets/frontend/info.zip", allow_redirects=True)
        if req.status_code == requests.codes.ok:
            open(get_default_config_path('info.zip'), 'wb').write(req.content)
            with ZipFile(get_default_config_path('info.zip'), 'r') as info_zip:
                info_zip.extractall(info_path)
        else:
            logger.error("Error retrieving libretro info archive from server: %s - %s", req.status_code, req.reason)
    # Parse info files to fetch display name and platform/system
    if os.path.exists(info_path):
        with os.scandir(info_path) as it:
            for entry in it:
                if entry.is_file():
                    core_identifier = entry.name.replace("_libretro.info", "")
                    core_config = RetroConfig(entry)
                    if "categories" in core_config.keys() and "Emulator" in core_config["categories"]:
                        core_label = core_config["display_name"] or ""
                        core_system = core_config["systemname"] or ""
                        LIBRETRO_CORES.append((core_label, core_identifier, core_system))
            LIBRETRO_CORES.sort(key=itemgetter(0))


def get_core_choices():
    return [(core[0], core[1]) for core in LIBRETRO_CORES]


class libretro(Runner):
    human_name = "Libretro"
    description = "Multi system emulator"
    runnable_alone = True
    runner_executable = "retroarch/retroarch"

    game_options = [
        {
            "option": "main_file",
            "type": "file",
            "label": "ROM file"
        },
        {
            "option": "core",
            "type": "choice",
            "label": "Core",
            "choices": get_core_choices(),
        },
    ]

    runner_options = [
        {
            "option": "config_file",
            "type": "file",
            "label": "Config file",
            "default": get_default_config_path("retroarch.cfg"),
        },
        {
            "option": "fullscreen",
            "type": "bool",
            "label": "Fullscreen",
            "default": True,
        },
        {
            "option": "verbose",
            "type": "bool",
            "label": "Verbose logging",
            "default": False,
        },
    ]

    @property
    def platforms(self):
        return [core[2] for core in LIBRETRO_CORES]

    def get_platform(self):
        game_core = self.game_config.get("core")
        if game_core:
            for core in LIBRETRO_CORES:
                if core[1] == game_core:
                    return core[2]
        return ""

    def get_core_path(self, core):
        return os.path.join(settings.RUNNER_DIR, "retroarch", "cores", "{}_libretro.so".format(core))

    def get_version(self, use_default=True):
        return self.game_config["core"]

    def is_retroarch_installed(self):
        return system.path_exists(self.get_executable())

    def is_installed(self, core=None):
        if self.game_config.get("core") and core is None:
            core = self.game_config["core"]
        if not core or self.runner_config.get("runner_executable"):
            return self.is_retroarch_installed()

        is_core_installed = system.path_exists(self.get_core_path(core))
        return self.is_retroarch_installed() and is_core_installed

    def install(self, version=None, downloader=None, callback=None):

        def install_core():
            if not version:
                if callback:
                    callback()
            else:
                super(libretro, self).install(version, downloader, callback)

        if not self.is_retroarch_installed():
            super(libretro, self).install(version=None, downloader=downloader, callback=install_core)
        else:
            super(libretro, self).install(version, downloader, callback)

    def get_run_data(self):
        return {
            "command": [self.get_executable()] + self.get_runner_parameters(),
            "env": self.get_env(),
        }

    def get_config_file(self):
        return self.runner_config.get("config_file") or get_default_config_path("retroarch.cfg")

    @staticmethod
    def get_system_directory(retro_config):
        """Return the system directory used for storing BIOS and firmwares."""
        system_directory = retro_config["system_directory"]
        if not system_directory or system_directory == "default":
            system_directory = get_default_config_path("system")
        return os.path.expanduser(system_directory)

    def prelaunch(self):
        # pylint: disable=too-many-locals,too-many-branches,too-many-statements
        config_file = self.get_config_file()
        # TODO: review later
        # Create retroarch.cfg if it doesn't exist.
        if not system.path_exists(config_file):
            f = open(config_file, "w")
            f.write("# Lutris RetroArch Configuration")
            f.close()

            # Build the default config settings.
            retro_config = RetroConfig(config_file)
            retro_config["libretro_directory"] = get_default_config_path("cores")
            retro_config["libretro_info_path"] = get_default_config_path("info")
            retro_config["content_database_path"] = get_default_config_path("database/rdb")
            retro_config["cheat_database_path"] = get_default_config_path("database/cht")
            retro_config["cursor_directory"] = get_default_config_path("database/cursors")
            retro_config["screenshot_directory"] = get_default_config_path("screenshots")
            retro_config["input_remapping_directory"] = get_default_config_path("remaps")
            retro_config["video_shader_dir"] = get_default_config_path("shaders")
            retro_config["core_assets_directory"] = get_default_config_path("downloads")
            retro_config["thumbnails_directory"] = get_default_config_path("thumbnails")
            retro_config["playlist_directory"] = get_default_config_path("playlists")
            retro_config["joypad_autoconfig_dir"] = get_default_config_path("autoconfig")
            retro_config["rgui_config_directory"] = get_default_config_path("config")
            retro_config["overlay_directory"] = get_default_config_path("overlay")
            retro_config["assets_directory"] = get_default_config_path("assets")
            retro_config.save()
        else:
            retro_config = RetroConfig(config_file)

        core = self.game_config.get("core")
        info_file = os.path.join(get_default_config_path("info"), "{}_libretro.info".format(core))
        if system.path_exists(info_file):
            retro_config = RetroConfig(info_file)
            try:
                firmware_count = int(retro_config["firmware_count"])
            except (ValueError, TypeError):
                firmware_count = 0
            system_path = self.get_system_directory(retro_config)
            notes = retro_config["notes"] or ""
            checksums = {}
            if notes.startswith("Suggested md5sums:"):
                parts = notes.split("|")
                for part in parts[1:]:
                    checksum, filename = part.split(" = ")
                    checksums[filename] = checksum
            for index in range(firmware_count):
                firmware_filename = retro_config["firmware%d_path" % index]
                firmware_path = os.path.join(system_path, firmware_filename)
                if system.path_exists(firmware_path):
                    if firmware_filename in checksums:
                        checksum = system.get_md5_hash(firmware_path)
                        if checksum == checksums[firmware_filename]:
                            checksum_status = "Checksum good"
                        else:
                            checksum_status = "Checksum failed"
                    else:
                        checksum_status = "No checksum info"
                    logger.info("Firmware '%s' found (%s)", firmware_filename, checksum_status)
                else:
                    logger.warning("Firmware '%s' not found!", firmware_filename)

                # Before closing issue #431
                # TODO check for firmware*_opt and display an error message if
                # firmware is missing
                # TODO Add dialog for copying the firmware in the correct
                # location

        return True

    def get_runner_parameters(self):
        parameters = []

        # Fullscreen
        fullscreen = self.runner_config.get("fullscreen")
        if fullscreen:
            parameters.append("--fullscreen")

        # Verbose
        verbose = self.runner_config.get("verbose")
        if verbose:
            parameters.append("--verbose")

        parameters.append("--config={}".format(self.get_config_file()))
        return parameters

    def play(self):
        command = [self.get_executable()]

        command += self.get_runner_parameters()

        # Core
        core = self.game_config.get("core")
        if not core:
            return {
                "error": "CUSTOM",
                "text": "No core has been selected for this game",
            }
        command.append("--libretro={}".format(self.get_core_path(core)))

        # Ensure the core is available
        if not self.is_installed(core):
            self.install(core)

        # Main file
        file = self.game_config.get("main_file")
        if not file:
            return {"error": "CUSTOM", "text": "No game file specified"}
        if not system.path_exists(file):
            return {"error": "FILE_NOT_FOUND", "file": file}
        command.append(file)
        return {"command": command}

    # Checks whether the retroarch or libretro directories can be uninstalled.
    def can_uninstall(self):
        retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
        return os.path.isdir(retroarch_path) or super(libretro, self).can_uninstall()

    # Remove the `retroarch` directory.
    def uninstall(self):
        retroarch_path = os.path.join(settings.RUNNER_DIR, 'retroarch')
        if os.path.isdir(retroarch_path):
            system.remove_folder(retroarch_path)
        super(libretro, self).uninstall()
